"""Handle outputting raw frigate frames"""

import datetime
import logging
import os
import shutil
import threading
from multiprocessing.synchronize import Event as MpEvent
from wsgiref.simple_server import make_server

from ws4py.server.wsgirefserver import (
    WebSocketWSGIHandler,
    WebSocketWSGIRequestHandler,
    WSGIServer,
)
from ws4py.server.wsgiutils import WebSocketWSGIApplication

from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
from frigate.comms.ws import WebSocket
from frigate.config import FrigateConfig
from frigate.config.camera.updater import (
    CameraConfigUpdateEnum,
    CameraConfigUpdateSubscriber,
)
from frigate.const import CACHE_DIR, CLIPS_DIR, PROCESS_PRIORITY_MED
from frigate.output.birdseye import Birdseye
from frigate.output.camera import JsmpegCamera
from frigate.output.preview import PreviewRecorder
from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame
from frigate.util.process import FrigateProcess

logger = logging.getLogger(__name__)


def check_disabled_camera_update(
    config: FrigateConfig,
    birdseye: Birdseye | None,
    previews: dict[str, PreviewRecorder],
    write_times: dict[str, float],
) -> None:
    """Check if camera is disabled / offline and needs an update."""
    now = datetime.datetime.now().timestamp()
    has_enabled_camera = False

    for camera, last_update in write_times.items():
        offline_time = now - last_update

        if config.cameras[camera].enabled:
            has_enabled_camera = True
        else:
            # flag camera as offline when it is disabled
            previews[camera].flag_offline(now)

        if offline_time > 1:
            # last camera update was more than 1 second ago
            # need to send empty data to birdseye because current
            # frame is now out of date
            if birdseye and offline_time < 10:
                # we only need to send blank frames to birdseye at the beginning of a camera being offline
                birdseye.write_data(
                    camera,
                    [],
                    [],
                    now,
                    get_blank_yuv_frame(
                        config.cameras[camera].detect.width,
                        config.cameras[camera].detect.height,
                    ),
                )

    if not has_enabled_camera and birdseye:
        birdseye.all_cameras_disabled()


class OutputProcess(FrigateProcess):
    def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
        super().__init__(
            stop_event, PROCESS_PRIORITY_MED, name="frigate.output", daemon=True
        )
        self.config = config

    def run(self) -> None:
        self.pre_run_setup(self.config.logger)

        frame_manager = SharedMemoryFrameManager()

        # start a websocket server on 8082
        WebSocketWSGIHandler.http_version = "1.1"
        websocket_server = make_server(
            "127.0.0.1",
            8082,
            server_class=WSGIServer,
            handler_class=WebSocketWSGIRequestHandler,
            app=WebSocketWSGIApplication(handler_cls=WebSocket),
        )
        websocket_server.initialize_websockets_manager()
        websocket_thread = threading.Thread(target=websocket_server.serve_forever)

        detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value)
        config_subscriber = CameraConfigUpdateSubscriber(
            self.config,
            self.config.cameras,
            [
                CameraConfigUpdateEnum.add,
                CameraConfigUpdateEnum.birdseye,
                CameraConfigUpdateEnum.enabled,
                CameraConfigUpdateEnum.record,
            ],
        )

        jsmpeg_cameras: dict[str, JsmpegCamera] = {}
        birdseye: Birdseye | None = None
        preview_recorders: dict[str, PreviewRecorder] = {}
        preview_write_times: dict[str, float] = {}
        failed_frame_requests: dict[str, int] = {}
        last_disabled_cam_check = datetime.datetime.now().timestamp()

        move_preview_frames("cache")

        for camera, cam_config in self.config.cameras.items():
            if not cam_config.enabled_in_config:
                continue

            jsmpeg_cameras[camera] = JsmpegCamera(
                cam_config, self.stop_event, websocket_server
            )
            preview_recorders[camera] = PreviewRecorder(cam_config)
            preview_write_times[camera] = 0

        if self.config.birdseye.enabled:
            birdseye = Birdseye(self.config, self.stop_event, websocket_server)

        websocket_thread.start()

        while not self.stop_event.is_set():
            # check if there is an updated config
            updates = config_subscriber.check_for_updates()

            if CameraConfigUpdateEnum.add in updates:
                for camera in updates["add"]:
                    jsmpeg_cameras[camera] = JsmpegCamera(
                        cam_config, self.stop_event, websocket_server
                    )
                    preview_recorders[camera] = PreviewRecorder(cam_config)
                    preview_write_times[camera] = 0

                    if (
                        self.config.birdseye.enabled
                        and self.config.cameras[camera].birdseye.enabled
                    ):
                        birdseye.add_camera(camera)

            (topic, data) = detection_subscriber.check_for_update(timeout=1)
            now = datetime.datetime.now().timestamp()

            if now - last_disabled_cam_check > 5:
                # check disabled cameras every 5 seconds
                last_disabled_cam_check = now
                check_disabled_camera_update(
                    self.config, birdseye, preview_recorders, preview_write_times
                )

            if not topic:
                continue

            (
                camera,
                frame_name,
                frame_time,
                current_tracked_objects,
                motion_boxes,
                _,
            ) = data

            if not self.config.cameras[camera].enabled:
                continue

            frame = frame_manager.get(
                frame_name, self.config.cameras[camera].frame_shape_yuv
            )

            if frame is None:
                logger.debug(f"Failed to get frame {frame_name} from SHM")
                failed_frame_requests[camera] = failed_frame_requests.get(camera, 0) + 1

                if (
                    failed_frame_requests[camera]
                    > self.config.cameras[camera].detect.fps
                ):
                    logger.warning(
                        f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues."
                    )

                continue
            else:
                failed_frame_requests[camera] = 0

            # send frames for low fps recording
            preview_recorders[camera].write_data(
                current_tracked_objects, motion_boxes, frame_time, frame
            )
            preview_write_times[camera] = frame_time

            # send camera frame to ffmpeg process if websockets are connected
            if any(
                ws.environ["PATH_INFO"].endswith(camera)
                for ws in websocket_server.manager
            ):
                # write to the converter for the camera if clients are listening to the specific camera
                jsmpeg_cameras[camera].write_frame(frame.tobytes())

            # send output data to birdseye if websocket is connected or restreaming
            if self.config.birdseye.enabled and (
                self.config.birdseye.restream
                or any(
                    ws.environ["PATH_INFO"].endswith("birdseye")
                    for ws in websocket_server.manager
                )
            ):
                birdseye.write_data(
                    camera,
                    current_tracked_objects,
                    motion_boxes,
                    frame_time,
                    frame,
                )

            frame_manager.close(frame_name)

        move_preview_frames("clips")

        while True:
            (topic, data) = detection_subscriber.check_for_update(timeout=0)

            if not topic:
                break

            (
                camera,
                frame_name,
                frame_time,
                current_tracked_objects,
                motion_boxes,
                regions,
            ) = data

            frame = frame_manager.get(
                frame_name, self.config.cameras[camera].frame_shape_yuv
            )
            frame_manager.close(frame_name)

        detection_subscriber.stop()

        for jsmpeg in jsmpeg_cameras.values():
            jsmpeg.stop()

        for preview in preview_recorders.values():
            preview.stop()

        if birdseye is not None:
            birdseye.stop()

        config_subscriber.stop()
        websocket_server.manager.close_all()
        websocket_server.manager.stop()
        websocket_server.manager.join()
        websocket_server.shutdown()
        websocket_thread.join()
        logger.info("exiting output process...")


def move_preview_frames(loc: str):
    preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache")
    preview_cache = os.path.join(CACHE_DIR, "preview_frames")

    try:
        if loc == "clips":
            shutil.move(preview_cache, preview_holdover)
        elif loc == "cache":
            if not os.path.exists(preview_holdover):
                return

            shutil.move(preview_holdover, preview_cache)
    except shutil.Error:
        logger.error("Failed to restore preview cache.")
